package com.googlecode.dgwt.client.dijit.base.widget;

import java.util.Iterator;
import java.util.Map;

import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.Event;
import com.googlecode.dgwt.client.dijit.Dijit;
import com.googlecode.dgwt.client.dijit.Widget;
import com.googlecode.dgwt.client.dijit.base.state.EventState;
import com.googlecode.dgwt.client.dijit.base.state.WidgetState;
import com.googlecode.dgwt.client.dijit.theme.CSS;
import com.googlecode.dgwt.client.dijit.theme.Theme;
import com.googlecode.dgwt.client.dijit.util.Template;
import com.googlecode.dgwt.client.dijit.util.TemplateHandler;
import com.googlecode.dgwt.client.dojo.Dojo;
import com.googlecode.dgwt.client.dojo.core.JsFunction;
import com.googlecode.dgwt.client.dojo.string.RegExp;
import com.googlecode.dgwt.client.dojo.util.JsUtil;
import com.googlecode.dgwt.client.dojo.util.StringMap;

/**
 * @author wellsantos@wellsantos.com
 * @since 0.01
 */
public class WidgetEngine implements Iterable<WidgetHandler>{

    public static final String CONTAINER_ELEMENT = "containerElement";
    public static final String SIZEABLE_ELEMENT = "sizeableElement";
    public static final String STYLE_ELEMENT = "styleElement";
    public static final String FOCUSABLE_ELEMENT = "focusableElement";
    public static final String SCROLLABLE_ELEMENT = "scrollableElement";
    

    /**
     * Attribute used to store an unique id into the {@link #domElement} of Widgets.
     */
    public static final String WIDGET_ID = "widgetid";

    /**
     * Auto incremented number to generate unique identifiers.
     * @see #WIDGET_ID
     */
    private static int generatedId = 0;    
    
    /**
     * Flag when {@link #startup()} method has been called.
     */
    private static final int STARTUP_FLAG = 0x01;
    
    /**
     * Flag when {@link #buildRendering()} method has been called.
     */
    private static final int BUILD_RENDERING_FLAG = 0x02;
    
    /**
     * Flag when {@link #postCreate()} method has been called.
     */
    private static final int POSTCREATE_FLAG = 0x04;
    

    /**
     * An unique id generated for this Widget.
     */
    protected final String widgetId;
    
    /**
     * The primary style name.
     */
    private String stylePrimaryName;
    
    /**
     * The root {@link Element} of nodes that this Widget has.
     */
    private Element domElement;
    
    /**
     * Map of elements attached to this Widget. 
     */
    private Map<String, Element> elementMap = new StringMap<Element>();
    
    /**
     * Map of attributes delegated to each element attached on this Widget.
     */
    private Map<String, String> attributeMap = new StringMap<String>();
    
    /**
     * Flags for this Widget.
     * @see #STARTUP_FLAG
     * @see #POSTCREATE_FLAG
     * @see #BUILD_RENDERING_FLAG
     */
    private int initFlags;

    /**
     * Represents a set of {@link WidgetState} that is current active.
     */
    private int activeStates;
    
    private int requiredEventStates;
    
    private final Widget widget;
    
    private UIHandler handler;
    

    public WidgetEngine(Widget targetWidget){
        widget = targetWidget;
        widgetId = WIDGET_ID + "_" + (++generatedId);
        stylePrimaryName = widgetId;
        setupHandler(widget.createHandler(), new DefaultHandler());
    }
    
    
    public static WidgetEngine get(Widget widget){
        return widget.getEngine();
    }
    
    public void addHandler(WidgetHandler nextHandler){
        assert(nextHandler != null);
        if (this.handler instanceof DefaultHandler){
            setupHandler(nextHandler, (DefaultHandler) handler);
        } else {
            ((WidgetHandler)this.handler).addHandler(nextHandler);
        }
        UIHandler savedNext = nextHandler.nextHandler;
        try {
            nextHandler.nextHandler = EmptyHandler.emptyHandler;
            nextHandler.widget = widget;
            nextHandler.initialize();
            
            if (isReady()){
                nextHandler.onPostCreate();
            }
            if (widget.isAttached()){
                nextHandler.doAttachChildren();
            }
            if (isStarted()){
                nextHandler.onStartup();
            }
            
        } finally {
            nextHandler.nextHandler = savedNext;
        }
    }
    
    public final UIHandler getHandler(){
        return handler;
    }
    
    public final Widget getWidget(){
        return widget;
    }
    
    public Element getDomElement(){
        return domElement;
    }
    
    /**
     * Sets the {@link #domElement} that is the root element for this Widget. <br/>
     * Note that this method should not be called after calling
     * {@link #postCreate()}.
     * 
     * @param domElement
     *            the {@link Element} object
     */
    public void setDomElement(Element domElement) {
        assert (!isReady()) : "DOM element was set after postCreate() has been called";
        this.domElement = domElement;
    }

    /**
     * This method is responsible to setup the Widget's configuration. <br/>
     * It's the first method called after Widget has been instantiated.
     */
    public void initialize() {
        for (WidgetHandler handler : this){
            handler.widget = widget;
        }
        getHandler().initialize();
    }

    /**
     * This method is responsible to create the Widget elements and render it's
     * contents. Subclasses that override this method can call the constructor
     * {@link #Widget(boolean)} passing True in parameter <code>ready</code>.
     */
    public void buildRendering() {
        getHandler().buildRendering();
    }

    /**
     * This method is responsible to setup the Widget elements. <br/>
     * Before this method be called, there is no guarantee that this Widget 
     * has any element assigned to.
     */
    public final void postCreate() {
        if ((initFlags & BUILD_RENDERING_FLAG) == 0) {
            initFlags |= BUILD_RENDERING_FLAG;

            widget.buildRendering();

            initFlags |= POSTCREATE_FLAG;

            assert (domElement != null) : Dojo.reflect.getSimpleName(getClass()) + ": DOM element was not set";
            widget.setElement(Element.as(domElement).<com.google.gwt.user.client.Element>cast());
            getDomElement().setAttribute(WIDGET_ID, widgetId);
            getDomElement().setId(JsUtil.<String>or(widget.getId(), widgetId));
            
            widget.getThemeCSS().applyTo(widget, true);
            widget.addStyleName(getStylePrimaryName());

            widget.onPostCreate();
        }
    }

    public void onPostCreate() {
        getHandler().onPostCreate();
    }

    /**
     * Called by {@link Dijit} after it loads.
     */
    public void startup() {
        if (!isStarted()) {
            initFlags |= STARTUP_FLAG;
            assert (isReady()) : Dojo.reflect.getSimpleName(getClass()) + ": postCreate() method was not called!";
            widget.onStartup();
        }
    }
    
    public void onStartup() {
        getHandler().onStartup();
    }

    /**
     * Checks if this Widget is ready to be used.
     * @return True if {@link #postCreate()} is already called
     */
    public boolean isReady(){
        return (initFlags & POSTCREATE_FLAG) != 0;
    }

    /**
     * Checks if this Widget is started.
     * 
     * @return True if this Widget was marked as started by {@link Dijit} module
     */
    public final boolean isStarted() {
        return (initFlags & STARTUP_FLAG) != 0;
    }

    public void loadFromTemplate(Template template) {
        loadFromTemplate(template, widget);
    }
    
    public void loadFromTemplate(Template template, TemplateHandler handler) {
        template.setSpanForCollection(true);
        
        template.load(handler);
        assert template.isLoaded() : Dojo.reflect.getSimpleName(getClass()) + ": Could not load template";
        
        for (String name : template.getAttachments().keySet()){
            setAttachedElement(name, template.getAttachments().get(name));
        }
        
        if (!template.getStyleSheets().isEmpty()){
            CSS templatedStyleSheet = Dijit.getTheme().createStyleSheet();
            
            for (String cssText : template.getStyleSheets()){
                templatedStyleSheet.addDeclaration(cssText);
            }
            
            templatedStyleSheet.importDeclaration();
        }
    }

    public void onThemeChanging(Theme newTheme) {
        getHandler().onThemeChanging(newTheme);
    }

    public void onThemeChanged(Theme oldTheme) {
        getHandler().onThemeChanged(oldTheme);
    }

    /**
     * Add an {@link WidgetState} to this Widget
     * @param state a WidgetState object
     */
    public void addState(WidgetState state) {
        if (!hasState(state)) {
            state.update(widget, true);
            this.activeStates |= state.getId();
        }
    }

    /**
     * Removes an {@link WidgetState} from this Widget
     * @param state a WidgetState object
     */
    public void removeState(WidgetState state) {
        if (hasState(state)) {
            state.update(widget, false);
            this.activeStates &= ~state.getId();
        }
    }

    /**
     * Checks if this Widget has a state
     * @param state a WidgetState object
     * @return True if it's present
     */
    public boolean hasState(WidgetState state) {
        return (this.activeStates & state.getId()) != 0;
    }
    
    /**
     * Add an {@link EventState} to be used by this Widget
     * and sink the related events assigned to the state.
     * @see #sinkEvents(int)
     * @param state an {@link EventState} object
     */
    public void requireEventState(EventState state) {
        requiredEventStates |= state.getId();
        widget.sinkEvents(state.getEventMask());
    }
    
    /* (non-Javadoc)
     * @see com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user.client.Event)
     */
    public void onBrowserEvent(Event event) {
        getHandler().onBrowserEvent(event);        
    }

    public void doAttachChildren() {
        getHandler().doAttachChildren();
    }
    
    public void doDetachChildren() {
        getHandler().doDetachChildren();
    }
    
    /**
     * Gets the element responsible for an attribute of this Widget.
     * 
     * @param name
     *            the attribute name
     *            
     * @see #getAttachedElement(String)
     * 
     * @return the name of an {@link Element} attached to this
     */
    public String getElementForAttribute(String name) {
        return attributeMap.get(name);
    }

    /**
     * Assigns an element name of any attached elements in this Widget to be
     * responsible for handle the specified attribute
     * 
     * @param name
     *            the attribute name
     *            
     * @see #getAttachedElement(String)
     * @see #setAttachedElement(String, Element)
     * 
     * @param element
     *            the name of an {@link Element} attached to this
     */
    public void setElementForAttribute(String name, String element) {
        attributeMap.put(name, element);
    }
    
    /**
     * Gets an {@link Element} attached to this Widget.
     * 
     * @param name
     *            the element name. e.g: "domElement", "styleElement",
     *            "sizeableElement", etc...
     * 
     * @see #getElement()
     * @see #getStyleElement()
     * @see #getSizeableElement()
     * @see #getContainerElement()
     *            
     * @return the {@link Element} object, or the root element ({@link #getElement()})  if
     *         there is no such element for the specified name
     */
    public Element getAttachedElement(String name) {
        Element element = elementMap.get(name);
        return element == null ? getDomElement() : element;
    }

    public Element getAttachedElement(String name, Element alternativeElement) {
        Element element = elementMap.get(name);
        return element == null ? alternativeElement : element;
    }

    public boolean hasAttachedElement(String name) {
        if (name.equals("domElement") || name.equals("element")){
            return domElement != null;
        }
        return elementMap.containsKey(name);
    }

    /**
     * Attach an element to this Widget. <br/>
     * <br/>
     * Note that it should be a child on the root {@link #domElement} children
     * tree. <br/>
     * Otherwise, it will not receive events from GWT be calling
     * {@link #sinkEvents(int)}.
     * 
     * @param name
     *            the element name. e.g: "domElement", "styleElement",
     *            "sizeableElement", etc...
     * @param element
     *            the {@link Element} object to attach
     */
    public void setAttachedElement(String name, Element element) {
        if (name.equals("domElement") || name.equals("element")){
            this.setDomElement(element);
        }
        else {
            elementMap.put(name, element);
        }
    }
    
    public void setStylePrimaryName(String newStylePrimaryName) {
        String styleName = widget.getStyleName();        
        newStylePrimaryName = newStylePrimaryName.trim();
        
        assert(!newStylePrimaryName.isEmpty()) : "Style names cannot be empty";

        if (!"".equals(styleName)) {
            String separator = Dijit.getTheme().getDependentSeparator();
            if ("".equals(separator)) {
                separator = "\\S*";
            } else {
                separator += "|\\s+";                
            }
            getDomElement().setClassName(
                    RegExp.create("(^|\\s+)("+stylePrimaryName+")("+separator+"|$)", "g").replace(
                    styleName, JsFunction.create("a","b","c","d", "return b+'" + newStylePrimaryName + "'+d;")));                
        }
        
        stylePrimaryName = newStylePrimaryName;
    }

    public String getStylePrimaryName() {
        return stylePrimaryName;
    }

    @Override
    public Iterator<WidgetHandler> iterator() {
        return new HandlerIterator<WidgetHandler>(){
            @Override
            protected WidgetHandler getTarget(UIHandler handler) {
                return (WidgetHandler) handler;
            }
        };
    }

    private void setupHandler(WidgetHandler previousHandler, DefaultHandler defaultHandler){
        if (previousHandler == null){
            this.handler = defaultHandler;
            return;
        }

        previousHandler = findLastHandler(previousHandler);
        assert (previousHandler.nextHandler == null) : "nextHandler already attached to another engine";
        previousHandler.nextHandler = defaultHandler;
        this.handler = previousHandler;
    }

    private WidgetHandler findLastHandler(WidgetHandler handler) {
        WidgetHandler prev = handler;
        UIHandler next = prev.nextHandler;
        while (next instanceof WidgetHandler) {
            prev = (WidgetHandler) next;
            next = prev.nextHandler;                
        }
        return prev;
    }

    
    /**
     * @author wellsantos@wellsantos.com
     * @since 0.01
     */
    class DefaultHandler implements UIHandler {
        
        @Override
        public void initialize() {
            
            //domElement
            setElementForAttribute(WIDGET_ID, "domElement");
            setElementForAttribute("style.margin", "domElement");
            setElementForAttribute("style.marginTop", "domElement");
            setElementForAttribute("style.marginLeft", "domElement");
            setElementForAttribute("style.marginRight", "domElement");
            setElementForAttribute("style.marginBottom", "domElement");
            
            //styleElement
            setElementForAttribute("style", "styleElement");

            //sizeableElement
            setElementForAttribute("style.width", "sizeableElement");
            setElementForAttribute("style.height", "sizeableElement");            
        }

        @Override
        public void buildRendering() {
        }

        @Override
        public void onBrowserEvent(Event event) {
            if (requiredEventStates != 0){
                int eventMask = event.getTypeInt();
                
                for (EventState state : EventState.getAllEventStates()) {
                    if ((requiredEventStates & state.getId()) == 0){
                        continue;
                    }
                    if (state.isActivatedBy(event, eventMask, widget)) {
                        addState(state);
                    } 
                    else if (state.isDeactivatedBy(event, eventMask, widget)) {
                        removeState(state);
                    }
                }
            }
            
            ((WidgetBase)widget).inheritedOnBrowserEvent(event);
        }

        @Override
        public void onPostCreate() {
        }

        @Override
        public void onStartup() {
        }

        @Override
        public void onThemeChanging(Theme newTheme) {
            int savedState = activeStates;
            for (WidgetState state : WidgetState.getAllWidgetStates()) {
                removeState(state);
            }
            activeStates = savedState;
        }
        
        @Override
        public void onThemeChanged(Theme oldTheme) {
            int savedState = activeStates;
            activeStates = 0;
            for (WidgetState state : WidgetState.getAllWidgetStates()) {
                if ((state.getId() & savedState) != 0) {
                    addState(state);
                }
            }
            widget.getThemeCSS().importDeclaration();
        }

        @Override
        public void doAttachChildren() {
            ((WidgetBase)widget).inheritedDoAttachChildren();
        }

        @Override
        public void doDetachChildren() {
            ((WidgetBase)widget).inheritedDoDetachChildren();
        }

        @Override
        public Widget getWidget() {
            return widget;
        }

    }

    
    /**
     * @author wellsantos@wellsantos.com
     * @since 0.01
     */
    static class EmptyHandler implements UIHandler {
        
        static final EmptyHandler emptyHandler = new EmptyHandler();

        @Override
        public void buildRendering() {
        }

        @Override
        public void doAttachChildren() {
        }

        @Override
        public void doDetachChildren() {
        }

        @Override
        public Widget getWidget() {
            return null;
        }

        @Override
        public void initialize() {
        }

        @Override
        public void onBrowserEvent(Event event) {
        }

        @Override
        public void onPostCreate() {
        }

        @Override
        public void onStartup() {
        }

        @Override
        public void onThemeChanged(Theme oldTheme) {
        }

        @Override
        public void onThemeChanging(Theme newTheme) {
        }
        
    }
    
    
    /**
     * @author wellsantos@wellsantos.com
     * @since 0.01
     */
    public abstract class HandlerIterator<H> implements Iterator<H> {

        UIHandler curHandler = handler; 
        
        private H getNext(boolean goAhead){
            UIHandler nextHandler = curHandler;
            
            while (!isInstance(nextHandler)) {
                
                if (nextHandler instanceof DefaultHandler){
                    return null;
                }
                
                nextHandler = ((WidgetHandler)nextHandler).nextHandler;
                curHandler = nextHandler;                
            }
            
            if (goAhead){
                curHandler = ((WidgetHandler)nextHandler).nextHandler;
            }
            
            return getTarget(nextHandler);
        }
        
        protected boolean isInstance(UIHandler handler){
            return handler instanceof WidgetHandler;
        }
        
        protected abstract H getTarget(UIHandler handler);
        
        @Override
        public boolean hasNext() {
            return getNext(false) != null;
        }

        @Override
        public H next() {
            H nextHandler = getNext(true);
            if (nextHandler == null){
                throw new IllegalStateException();
            }                
            return nextHandler;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

    }
    
}
